CR-133: Report terminal revocation as healthy-as-revoked (implementation slice) - #178
Merged
coreytshaffer merged 2 commits intoAug 15, 2026
Merged
Conversation
Implements the CR-133 accepted option (a) semantics:
LifecycleHealthy != OperationallyUsable != CapabilityReady
At main@770d9f2 a revoked identity produced `no_active_key` plus
`missing_rotated_at` and `missing_archived_key`, conflating a valid terminal
lifecycle state with a fault. The two warnings were structurally unreachable
as true, since revocation neither stamps rotated_at nor archives key material.
Suppressing those findings alone would have introduced a worse defect: the
`--for-capability` loop skips agents without exactly one active identity and
the process exits non-zero only on report errors, so the invocation would have
exited 0 in silence, reporting success for an identity that is not capability
ready. Both surfaces are therefore changed together.
A shared module-level predicate classifies the accepted state narrowly -- zero
active, exactly one revoked, every remaining record rotated -- so check_health()
and tc_identity_doctor() cannot drift apart on the classification.
Preserved: ROTATED_STATUS archival diagnostics; COMPROMISED_STATUS health
behavior through both the zero-active and historical-record paths;
`no_active_key` for every other zero-active cause; revoked identities remain
unusable for signing, verification, authorization, and capability readiness;
IdentityDoctorReport and IdentityDoctorIssue unchanged; no private-key
disposition introduced or inferred.
`--for-capability` fails via a distinct `revoked_identity_not_capability_ready`
code rather than reusing `missing_requested_capability`, which would assert
that the capability is absent when revocation is the operative reason.
Scope: the four-file allowlist recorded in CR-133. Full suite 1718 passed,
6 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Deploy Preview for poetic-quokka-0fd859 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
The regression suite asserted printed status but not the process exit code:
run_cli_command() swallowed SystemExit and returned stdout only. CR-133's
post-change-state trap is specifically an exit-code defect -- a revoked identity
exiting 0 while emitting no capability finding -- so the contract that matters
most was unpinned.
Adds run_cli_command_with_exit_code() returning (stdout, exit_code); the
existing helper delegates to it, so no pre-existing test changes shape. main()
returning without raising is treated as exit 0.
Pinned:
doctor <terminal-revoked> -> 0
doctor <terminal-revoked> --for-capability -> 1
doctor <active, capability present> -> 0 (positive control)
doctor <active, capability absent> -> 1 (negative control)
Verified load-bearing by mutation: changing only the doctor's terminal
sys.exit(1) to sys.exit(0), leaving all printed output byte-identical, fails
exactly the three exit-code-pinned failure cases and nothing else.
Full suite 1718 passed, 6 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coreytshaffer
added a commit
that referenced
this pull request
Aug 14, 2026
…nd exhausted The CR text still read "Implementation authority: Still withheld", which no longer matched the governance state: a bounded single-slice grant was made by the human operator on 2026-08-14 and exercised as PR #178. The durable record had not caught up with the decision. Records the grant verbatim as its own section -- four-file allowlist, the shared terminal-revoked helper explicitly in scope, the narrowly bounded predicate, the permitted changes, and the preserved constraints -- then notes that the grant is exhausted now that the reviewable candidate exists, and that implementation acceptance was reviewed and withheld pending two evidence repairs. Also repoints statements the grant would otherwise contradict: Scope, Human Approval Requirement, Required Regression Set, Explicit Exclusions, Deferred Work, and Stop Point, which now stops a fourth time before acceptance rather than a third time before implementation. The deferred archived-design coverage gaps are stated as still outside the grant. Recorded on the CR branch rather than folded into PR #178, and without rewriting a5ed999. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coreytshaffer
added a commit
that referenced
this pull request
Aug 14, 2026
…bb36d4 Implementation acceptance was granted by the human operator on 2026-08-14, following implementation-design review, code review, and two evidence repairs (the authority record at f952e26, the exit-code pins at bbb36d4). Acceptance attaches to that specific revision and settles correctness, not landing. Repoints the passages the grant makes stale: the acceptance withholding is restated as first-review history with its resolution, and the Stop Point now stops a fifth time before merge authority rather than a fourth time before acceptance. Records the recommended merge ordering -- #177 first, then #178 -- with its rationale. The two PRs have no file overlap, so nothing forces that order, which is why it is written down: landing the authority and acceptance record before the runtime change it authorized gives repository history the same causal order as the governance process. Merge, release, and closeout authority remain ungranted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the CR-133 accepted option (a) semantics under the single-slice
implementation-authority grant recorded on 2026-08-14, bounded to the four-file
allowlist.
Recorded pre-change behavior
Measured read-only against
main@770d9f2for a revoked identity:The two warnings were structurally unreachable as true:
revoke_identity()neither stamps
rotated_atnor archives key material, so a revoked record wasbeing measured against rotation-shaped expectations it can never satisfy.
Why both surfaces change together
A change confined to
check_health()would have introduced a worse defect. The--for-capabilityloop skips any agent without exactly one active identity, andthe process exits non-zero only when the report has errors. Today's non-zero exit
for a revoked agent comes entirely from
no_active_key. Removing it alone makestc identity doctor <revoked> --for-capability Xexit 0 in silence — emittingneither a capability error nor a readiness line — reporting success for an identity
the accepted semantics state is not capability-ready.
Approach
A shared module-level predicate,
is_terminal_revoked(), classifies the acceptedstate narrowly — zero
ACTIVE_STATUS, exactly oneREVOKED_STATUS, everyremaining record
ROTATED_STATUS. Bothcheck_health()andtc_identity_doctor()consume it, so the two surfaces cannot drift apart on the classification.
Reachability of the refused shapes was probed rather than assumed: multiply-revoked
is unreachable through supported commands (
identity initrefuses an existingagent_id), and
COMPROMISED_STATUShas no production writer. Both are hand-editonly, and both keep their current findings.
--for-capabilityfails via a distinctrevoked_identity_not_capability_readycode. Reusing
missing_requested_capabilitywould assert the capability is absentwhen revocation is the operative reason — a different and often false fact. The
message claims nothing about whether the capability was ever granted, so it holds
for both the granted and never-granted cases.
The revoked state is stated positively rather than merely non-erroring, so a bare
Identity doctor passedcannot be misread as "this signer is ready":Constraints preserved
ROTATED_STATUShistorical-integrity diagnostics intact — proven by a test thatdeletes the archive on a rotated-then-revoked agent and asserts the warning still
fires against the rotated fingerprint and not the revoked one
COMPROMISED_STATUShealth behavior unchanged through both the zero-active andhistorical-record paths. The historical guard stays a negative exemption rather
than being rewritten to
status == ROTATED_STATUS, and suppression is conditionedon terminal revocation rather than on "no active identity" — the two vectors
CR-133 names
no_active_keyintact for every other zero-active causecapability readiness —
verify_signed_payload()andrequire_authorized_capability()untouchedIdentityDoctorReport,IdentityDoctorIssue,revoke_identity(), andcheck_consistency()unchangedVerification
Full suite: 1718 passed, 6 skipped. Nine new cases in
tests/test_doctor_cli.pycover the CR's four required regression cases plus a compromised-state behavioral
non-change proof, a parametrized guard against widening the predicate, a read-only
guarantee on the revoked path, and narrow
check/doctornon-contradiction for therevoked state only.
Scope
Independent of PR #177, which adds only the CR document — no file overlap. This
branch is based at
origin/main@770d9f2.The grant covers preparation of this reviewable candidate only. It does not carry
implementation acceptance, merge, release, or closeout authority. The four deferred
archived-design coverage gaps remain outside this slice.
🤖 Generated with Claude Code